package com.ming.mallchat.common.websocket.domain.service.impl;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ming.mallchat.common.common.config.ThreadPoolConfig;
import com.ming.mallchat.common.common.event.UserOnlineEvent;
import com.ming.mallchat.common.user.dao.UserDao;
import com.ming.mallchat.common.user.domain.enums.RoleEnum;
import com.ming.mallchat.common.user.domain.pojos.IpInfo;
import com.ming.mallchat.common.user.pojo.User;
import com.ming.mallchat.common.user.service.LoginService;
import com.ming.mallchat.common.user.service.RoleService;
import com.ming.mallchat.common.user.service.UserRoleService;
import com.ming.mallchat.common.websocket.domain.adapter.WebSocketAdapter;
import com.ming.mallchat.common.websocket.domain.dto.WSChannelExtraDto;
import com.ming.mallchat.common.websocket.domain.service.WebSocketService;
import com.ming.mallchat.common.websocket.domain.utils.NettyUtils;
import com.ming.mallchat.common.websocket.domain.vo.req.WSBaseReq;
import com.ming.mallchat.common.websocket.domain.vo.resp.WSBaseResp;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;

@Service
public class WebSocketServiceImpl implements WebSocketService {

    @Autowired
    @Lazy
    private WxMpService wxMpService;

    @Autowired
    private UserDao userDao;

    @Autowired
    private LoginService loginService;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private RoleService roleService;

    @Autowired
    @Qualifier(ThreadPoolConfig.WS_EXECUTOR)
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    /**
     * 管理所有用户的连接 （游客、登录态）
     */
    private static final ConcurrentHashMap<Channel, WSChannelExtraDto> ONLINE_WS_MAP = new ConcurrentHashMap<>();

    public static final int MAXIMUM_SIZE = 1000;
    public static final Duration DURATION = Duration.ofHours(1);
    private static final Cache<Integer, Channel> WAIT_LOGIN_MAP = Caffeine.newBuilder()
            .maximumSize(MAXIMUM_SIZE)
            .expireAfterAccess(DURATION)
            .build();

    /**
     * webSocket连接保存
     *
     * @param ctx
     */
    @Override
    public void connect(Channel ctx) {
        ONLINE_WS_MAP.put(ctx, new WSChannelExtraDto());
    }

    /**
     * 登入暂时保存
     *
     * @param channel
     */
    @Override
    public void handleLoginReq(Channel channel) {
        //获取随机code,并存入临时登入池
        Integer code = generateLoginCode(channel);
        try {
            //通过微信获取登入二维码
            WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService()
                    .qrCodeCreateTmpTicket(code, (int) DURATION.getSeconds());
            //将二维码等信息推送给前端
            sendMsg(channel, WebSocketAdapter.buildResp(wxMpQrCodeTicket));
        } catch (WxErrorException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 登入成功获取channel上线
     *
     * @param code
     * @param id
     */
    @Override
    public void scanLoginSuccess(Integer code, Long id) {
        //确认连接在机器上
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(code);
        if (Objects.isNull(channel)) {
            return;
        }
        User user = userDao.getById(id);
        //移除code
        WAIT_LOGIN_MAP.invalidate(code);
        //调用登入模块获取token
        String token = loginService.login(id);
        //用户登入
        loginSuccess(channel, user, token);
    }

    /**
     * 等待用户授权
     *
     * @param code
     */
    @Override
    public void waitAuthor(Integer code) {
        //确认连接在机器上
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(code);
        if (Objects.isNull(channel)) {
            return;
        }
        sendMsg(channel, WebSocketAdapter.buildWaitAuthorResp());
    }

    /**
     * 用户刷新重新授权
     *
     * @param channel
     * @param token
     */
    @Override
    public void author(Channel channel, String token) {
        Long uid = loginService.getValidUid(token);
        if (Objects.nonNull(uid)) {
            User user = userDao.getById(uid);
            loginSuccess(channel, user, token);
        } else {
            sendMsg(channel, WebSocketAdapter.buildInvalidTokenResp());
        }
    }

    /**
     * 用户下线
     *
     * @param ctx
     */
    @Override
    public void remove(ChannelHandlerContext ctx) {
        ONLINE_WS_MAP.remove(ctx);
    }

    /**
     * 通知所有用户，有人被拉黑
     *
     * @param msg
     */
    @Override
    public void sendMsgToAll(WSBaseResp<?> msg) {
        ONLINE_WS_MAP.forEach((channel, ext) -> {
            threadPoolTaskExecutor.execute(() -> {
                sendMsg(channel, msg);
            });
        });
    }

    /**
     * 推送登入成功的消息
     * 保存channel以及用户上线
     *
     * @param channel
     * @param user
     * @param token
     */
    private void loginSuccess(Channel channel, User user, String token) {
        //保存channel与uid的关系
        WSChannelExtraDto wsChannelExtraDto = ONLINE_WS_MAP.get(channel);
        wsChannelExtraDto.setUid(user.getId());
        //推送登入成功的消息给前端
        sendMsg(channel, WebSocketAdapter.buildResp(user, token, roleService.hasRole(user.getId())));
        //用户上线成功的事件
        user.refreshIp(NettyUtils.getAttr(channel, NettyUtils.IP));
        user.setLastOptTime(new Date());
        applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));
    }

    private void sendMsg(Channel channel, WSBaseResp<?> wsBaseResp) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
    }


    private Integer generateLoginCode(Channel channel) {
        Integer code = null;
        do {
            code = RandomUtil.randomInt(Integer.MAX_VALUE);
        } while (Objects.nonNull(WAIT_LOGIN_MAP.asMap().putIfAbsent(code, channel)));
        return code;
    }


}
